﻿// VsPkg.cs : Implementation of TTXPathNavigator
//

using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.ComponentModel.Design;
using Microsoft.Win32;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using EnvDTE;
using Microsoft.VisualStudio.TextManager.Interop;
using System.Xml;
using System.IO;
using System.Xml.XPath;
using System.Collections.Generic;

namespace TTRider.TTXPathNavigator
{
    /// <summary>
    /// This is the class that implements the package exposed by this assembly.
    ///
    /// The minimum requirement for a class to be considered a valid package for Visual Studio
    /// is to implement the IVsPackage interface and register itself with the shell.
    /// This package uses the helper classes defined inside the Managed Package Framework (MPF)
    /// to do it: it derives from the Package class that provides the implementation of the 
    /// IVsPackage interface and uses the registration attributes defined in the framework to 
    /// register itself and its components with the shell.
    /// </summary>
    // This attribute tells the registration utility (regpkg.exe) that this class needs
    // to be registered as package.
    [PackageRegistration(UseManagedResourcesOnly = true)]
    // A Visual Studio component can be registered under different regitry roots; for instance
    // when you debug your package you want to register it in the experimental hive. This
    // attribute specifies the registry root to use if no one is provided to regpkg.exe with
    // the /root switch.
    [DefaultRegistryRoot("Software\\Microsoft\\VisualStudio\\9.0")]
    // This attribute is used to register the informations needed to show the this package
    // in the Help/About dialog of Visual Studio.
    [InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]
    // In order be loaded inside Visual Studio in a machine that has not the VS SDK installed, 
    // package needs to have a valid load key (it can be requested at 
    // http://msdn.microsoft.com/vstudio/extend/). This attributes tells the shell that this 
    // package has a load key embedded in its resources.
    [ProvideLoadKey("Standard", "1.0", "TTXPathNavigator", "TTRider", 1)]
    // This attribute is needed to let the shell know that this package exposes some menus.
    [ProvideMenuResource(1000, 1)]
    // This attribute registers a tool window exposed by this package.
    [Guid(CommandGuids.guidTTXPathNavigatorPkgString)]
    public sealed class TTXPathNavigatorPackage : Package
    {
        /// <summary>
        /// GUID for Output window pane
        /// </summary>
        Guid outputGuid = new Guid("{AD72592B-06BC-4e75-8149-B35901767820}");

        /// <summary>
        /// default prefix for 'xmlns' namespace
        /// </summary>
        string defaultNamespacePrefix = "ns"; //TODO make it comfigurable

        /// <summary>
        /// XPAth combox command
        /// </summary>
        OleMenuCommand xpathCombobox;

        /// <summary>
        /// Default constructor of the package.
        /// Inside this method you can place any initialization code that does not require 
        /// any Visual Studio service because at this point the package object is created but 
        /// not sited yet inside Visual Studio environment. The place to do all the other 
        /// initialization is the Initialize method.
        /// </summary>
        public TTXPathNavigatorPackage()
        {
        }

        /// <summary>
        /// Initialization of the package; this method is called right after the package is sited, so this is the place
        /// where you can put all the initilaization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize()
        {
            base.Initialize();

            // Add our command handlers for menu (commands must exist in the .vsct file)
            OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
            if ( null != mcs )
            {
                xpathCombobox = new OleMenuCommand(OnXPathCommand, guidTTXPathNavigatorCmdSet.cmdidXPathComboCmd);
                xpathCombobox.ParametersDescription = "$"; // accept any argument string

                xpathCombobox.BeforeQueryStatus += delegate
                {
                    xpathCombobox.Enabled = GetActiveTextDocument() != null;
                };

                mcs.AddCommand(xpathCombobox);
            }
        }

        /// <summary>
        /// Creates OutputPane
        /// </summary>
        /// <returns></returns>
        IVsOutputWindowPane GetOutputPane()
        {
            IVsOutputWindowPane ret = null;
            IVsOutputWindow wnd = (IVsOutputWindow)GetService(typeof(IVsOutputWindow));
            if (wnd != null)
            {
                wnd.GetPane(ref outputGuid, out ret);

                if (ret == null)
                {
                    wnd.CreatePane(ref outputGuid, Resources.XPathNavigator, 0, 1);
                    wnd.GetPane(ref outputGuid, out ret);
                }
            }

            return ret;
        }


        string GetXPathQuery(EventArgs e)
        {
            OleMenuCmdEventArgs eventArgs = e as OleMenuCmdEventArgs;
            if (eventArgs != null)
            {
                object input = eventArgs.InValue;

                if (input != null)
                {
                    return input.ToString();
                }
            }
            return null;
        }

        TextDocument GetActiveTextDocument()
        {
            _DTE dte = (_DTE)GetService(typeof(_DTE));
            if ((dte != null) && (dte.ActiveWindow != null) && (dte.ActiveWindow.Document != null) && (string.Equals(dte.ActiveWindow.Document.Language, "XML", StringComparison.OrdinalIgnoreCase)))
            {
                return dte.ActiveDocument.Object("") as TextDocument;
            }
            return null;
        }

        /// <summary>
        /// This function is the callback used to execute a command when the a menu item is clicked.
        /// See the Initialize method to see how the menu item is associated to this function using
        /// the OleMenuCommandService service and the MenuCommand class.
        /// </summary>
        private void OnXPathCommand(object sender, EventArgs e)
        {
            try
            {
                string xpathQuery = GetXPathQuery(e);
                if (!string.IsNullOrEmpty(xpathQuery))
                {
                    TextDocument td = GetActiveTextDocument();
                    if (td != null)
                    {
                        EditPoint sp = td.CreateEditPoint(td.StartPoint);
                        EditPoint ep = td.CreateEditPoint(td.EndPoint);

                        XPathTextManager manager = new XPathTextManager(new StringReader(sp.GetText(ep)), defaultNamespacePrefix);

                        IVsOutputWindowPane pane = GetOutputPane();
                        if (pane != null)
                        {
                            pane.Clear();
                            pane.Activate();
                            pane.OutputString(string.Format(Resources.Query, xpathQuery));

                            int count = 0;
                            foreach (XmlNodeInfo info in manager.Select(xpathQuery))
                            {
                                string xmlSample = info.Navigator.OuterXml;
                                if (xmlSample.Length > 1024)
                                {
                                    xmlSample = xmlSample.Substring(0, 1024) + " ...";
                                }

                                xmlSample = xmlSample.Replace("\r", "").Replace("\n", " ");


                                string output = string.Format(Resources.OutputString, info.LineNumber, info.LinePosition, xmlSample);


                                IVsOutputWindowPane2 pane2 = pane as IVsOutputWindowPane2;
                                if (pane2 == null)
                                {
                                    pane.OutputTaskItemString(output, VSTASKPRIORITY.TP_NORMAL, VSTASKCATEGORY.CAT_SHORTCUTS, Resources.XPathNavigator, 0, td.DTE.ActiveDocument.FullName, (uint)info.LineNumber - 1, output);
                                }
                                else
                                {
                                    pane2.OutputTaskItemStringEx2(output, VSTASKPRIORITY.TP_NORMAL, VSTASKCATEGORY.CAT_SHORTCUTS, Resources.XPathNavigator, (int)_vstaskbitmap.BMP_SHORTCUT, td.DTE.ActiveDocument.FullName, (uint)info.LineNumber - 1, (uint)info.LinePosition - 1, null, output, null);
                                }
                                count++;
                            }

                            pane.OutputString(string.Format((count == 1) ? Resources.ResultSingle : Resources.ResultMult, count));
                            pane.FlushToTaskList();

                            if ((count == 0) && (manager.HasDefaultNamespace) && (xpathQuery.IndexOf(this.defaultNamespacePrefix + ":") == -1))
                            {
                                pane.OutputString(Resources.ResultZero);
                            }
                        }
                    }
                }

            }
            catch (Exception ex)
            {
                VsShellUtilities.ShowMessageBox(this, ex.Message, Resources.Error, OLEMSGICON.OLEMSGICON_CRITICAL, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
            }
        }


        class XPathNavComparer : IEqualityComparer<XPathNavigator>
        {

            #region IEqualityComparer<XPathNavigator> Members

            public bool Equals(XPathNavigator x, XPathNavigator y)
            {
                return XPathNavigator.NavigatorComparer.Equals(x, y);
            }

            public int GetHashCode(XPathNavigator obj)
            {
                return XPathNavigator.NavigatorComparer.GetHashCode(obj);
            }

            #endregion
        }


        public class XmlNodeInfo
        {
            int lineNumber;
            int linePosition;
            string name;
            XPathNavigator navigator;

            public XmlNodeInfo(int lineNumber, int linePosition, string name)
            {
                this.lineNumber = lineNumber;
                this.linePosition = linePosition;
                this.name = name;
            }

            public string Name { get { return this.name; } }
            public int LineNumber { get { return this.lineNumber; } }
            public int LinePosition { get { return this.linePosition; } }
            public XPathNavigator Navigator { get { return this.navigator; } set { this.navigator = value; } }
        }

        public class XmlElementInfo : XmlNodeInfo
        {
            Dictionary<string, XmlNodeInfo> attributes;

            public XmlElementInfo(int lineNumber, int linePosition, string name)
                : base(lineNumber, linePosition, name)
            {
            }

            public Dictionary<string, XmlNodeInfo> Attributes
            {
                get
                {
                    if (this.attributes == null)
                    {
                        this.attributes = new Dictionary<string, XmlNodeInfo>();
                    }
                    return this.attributes;
                }
            }
        }


        public class XmlLineInfoReader : XmlTextReader
        {
            List<XmlNodeInfo> elements = new List<XmlNodeInfo>();

            public XmlLineInfoReader(TextReader s)
                : base(s)
            {
            }

            public List<XmlNodeInfo> Elements
            {
                get { return this.elements; }
            }

            public override bool Read()
            {
                bool retVal = base.Read();

                if (retVal)
                {
                    if (this.NodeType == XmlNodeType.Element)
                    {
                        XmlElementInfo elInfo = new XmlElementInfo(this.LineNumber, this.LinePosition, this.Name);
                        elements.Add(elInfo);

                        // let's get information about attributes
                        if (this.HasAttributes)
                        {
                            if (this.MoveToFirstAttribute())
                            {
                                XmlNodeInfo aInfo = new XmlNodeInfo(this.LineNumber, this.LinePosition, this.Name);
                                elInfo.Attributes[this.Name] = aInfo;

                                while (this.MoveToNextAttribute())
                                {
                                    aInfo = new XmlNodeInfo(this.LineNumber, this.LinePosition, this.Name);
                                    elInfo.Attributes[this.Name] = aInfo;
                                }

                                this.MoveToElement();
                            }
                        }
                    }
                }
                return retVal;
            }
        }


        public class XPathTextManager
        {
            XPathDocument document;
            Dictionary<XPathNavigator, XmlNodeInfo> navigators = new Dictionary<XPathNavigator, XmlNodeInfo>(new XPathNavComparer());
            XPathNavigator root;
            XmlNamespaceManager namespaceManager;
            string prefixForDefault;
            bool hasDefaultNamespace;

            public XPathTextManager(TextReader reader, string prefixForDefault)
            {
                this.prefixForDefault = prefixForDefault;

                XmlLineInfoReader lir = new XmlLineInfoReader(reader);

                this.document = new XPathDocument(lir);
                this.root = this.document.CreateNavigator();
                this.namespaceManager = new XmlNamespaceManager(this.root.NameTable);

                XPathNodeIterator iterator = this.root.SelectDescendants(XPathNodeType.Element, true);

                int i = 0;
                foreach (XPathNavigator item in iterator)
                {
                    foreach (KeyValuePair<string, string> de in item.GetNamespacesInScope(XmlNamespaceScope.All))
                    {
                        if (string.IsNullOrEmpty(de.Key))
                        {
                            this.namespaceManager.AddNamespace(prefixForDefault, de.Value);
                            this.hasDefaultNamespace = true;
                        }
                        else
                        {
                            this.namespaceManager.AddNamespace(de.Key, de.Value);
                        }
                    }

                    XmlNodeInfo info = lir.Elements[i++];
                    info.Navigator = item;
                    this.navigators[item] = info;
                    if (i >= lir.Elements.Count)
                    {
                        break;
                    }
                }
            }

            T GetNodeInfo<T>(XPathNavigator item) where T : XmlNodeInfo
            {
                XmlNodeInfo info;
                if (this.navigators.TryGetValue(item, out info))
                {
                    info.Navigator = item;
                    return info as T;
                }
                return default(T);
            }

            public IEnumerable<XmlNodeInfo> Select(string query)
            {
                XPathExpression exp = XPathExpression.Compile(query, this.namespaceManager);
                foreach (XPathNavigator item in this.root.Select(exp))
                {
                    switch (item.NodeType)
                    {
                        case XPathNodeType.Attribute:
                            {
                                XPathNavigator nav = item.CreateNavigator();
                                if (nav.MoveToParent())
                                {
                                    XmlElementInfo info = GetNodeInfo<XmlElementInfo>(nav);
                                    if (info != null)
                                    {
                                        XmlNodeInfo ainfo;
                                        if (info.Attributes.TryGetValue(item.Name, out ainfo))
                                        {
                                            ainfo.Navigator = item;
                                            yield return ainfo;
                                        }
                                    }
                                }
                            }
                            break;

                        case XPathNodeType.Element:
                            {
                                XmlElementInfo info = GetNodeInfo<XmlElementInfo>(item);
                                if (info != null)
                                {
                                    info.Navigator = item;
                                    yield return info;
                                }
                                break;
                            }
                    }
                }
            }

            public bool HasDefaultNamespace { get { return this.hasDefaultNamespace; } }
        }

    }
}